module sdram_fifo #(
    parameter DEPTH = 8,
    parameter WIDTH = 32,
    parameter ADDR_WIDTH = $clog2(DEPTH)
)(
    input                   clk,
    input                   rst_n,

    input                   we,
    input [WIDTH-1:0]       wdata,
    output [ADDR_WIDTH:0]   remain_cnt,

    input                   re,
    output                  rvalid,
    output [WIDTH-1:0]      rdata
);
logic [ADDR_WIDTH-1: 0] head, tail;
logic hdir, tdir;
logic [ADDR_WIDTH: 0] remain_count;
logic [DEPTH-1:0][WIDTH-1:0] mem;

wire empty = (head == tail) & (hdir == tdir);

always_ff @(posedge clk)begin
    if(~rst_n)begin
        head <= 0;
        tail <= 0;
        hdir <= 0;
        tdir <= 0;
        remain_count <= DEPTH;
    end
    else begin
        if(we ^ re)begin
            if(we)begin
                remain_count <= remain_count - 1;
            end
            else begin
                remain_count <= remain_count + 1;
            end
        end
        if(we)begin
            tail <= tail + 1;
            if(&tail)begin
                tdir <= ~tdir;
            end
        end
        if(re)begin
            head <= head + 1;
            if(&head)begin
                hdir <= ~hdir;
            end
        end
    end
end

always_ff @(posedge clk)begin
    if(we)begin
        mem[tail] <= wdata;
    end
end

assign rdata = mem[head];
assign rvalid = ~empty;
assign remain_cnt = remain_count;

endmodule

module gray_to_binary #(
    parameter int N = -1
)(
    input  logic [N-1:0] A,
    output logic [N-1:0] Z
);
    for (genvar i = 0; i < N; i++)
        assign Z[i] = ^A[N-1:i];
endmodule

module binary_to_gray #(
    parameter int N = -1
)(
    input  logic [N-1:0] A,
    output logic [N-1:0] Z
);
    assign Z = A ^ (A >> 1);
endmodule

module sync #(
    parameter int unsigned STAGES = 2,
    parameter bit ResetValue = 1'b0
) (
    input  logic clk_i,
    input  logic rst_ni,
    input  logic serial_i,
    output logic serial_o
);

   (* dont_touch = "true" *)
   (* async_reg = "true" *)
   logic [STAGES-1:0] reg_q;

    always_ff @(posedge clk_i, negedge rst_ni) begin
        if (!rst_ni) begin
            reg_q <= {STAGES{ResetValue}};
        end else begin
            reg_q <= {reg_q[STAGES-2:0], serial_i};
        end
    end

    assign serial_o = reg_q[STAGES-1];

endmodule

module sdram_async_fifo #(
    parameter DEPTH = 4,
    parameter WIDTH = 32,
    parameter ADDR_WIDTH = $clog2(DEPTH)
)(
    input                   src_clk,
    input                   src_rst_n,
    input [WIDTH-1:0]       src_wdata,
    input                   src_valid,

    input                   dst_clk,
    input                   dst_rst_n,
    output [WIDTH-1:0]      dst_rdata,
    output                  dst_valid,
    input                   dst_ready
);

    logic [DEPTH-1:0][WIDTH-1:0] mem;
    typedef struct packed {
        logic dir;
        logic [ADDR_WIDTH-1:0] idx;
    } FifoPtr;
    FifoPtr src_ptr, dst_ptr;
    FifoPtr src_ptr_sync, dst_ptr_sync;
    FifoPtr src_ptr_sync_bin, dst_ptr_sync_bin;
    FifoPtr src_ptr_bin, dst_ptr_bin;
    FifoPtr src_ptr_bin_n, dst_ptr_bin_n;
    FifoPtr src_ptr_gray, dst_ptr_gray;

    gray_to_binary #(
        .N(ADDR_WIDTH+1)
    ) gray_to_bin_src (
        .A(src_ptr),
        .Z(src_ptr_bin)
    );
    assign src_ptr_bin_n = src_ptr_bin + 1;
    binary_to_gray #(
        .N(ADDR_WIDTH+1)
    ) bin_to_gray_src (
        .A(src_ptr_bin_n),
        .Z(src_ptr_gray)
    );

    gray_to_binary #(
        .N(ADDR_WIDTH+1)
    ) gray_to_bin_dst (
        .A(dst_ptr),
        .Z(dst_ptr_bin)
    );
    assign dst_ptr_bin_n = dst_ptr_bin + 1;
    binary_to_gray #(
        .N(ADDR_WIDTH+1)
    ) bin_to_gray_dst (
        .A(dst_ptr_bin_n),
        .Z(dst_ptr_gray)
    );

generate
    for(genvar i=0; i<ADDR_WIDTH+1; i++)begin
        sync #(
            .STAGES(2),
            .ResetValue(1'b0)
        ) sync_src (
            .clk_i(dst_clk),
            .rst_ni(dst_rst_n),
            .serial_i(src_ptr[i]),
            .serial_o(src_ptr_sync[i])
        );

        sync #(
            .STAGES(2),
            .ResetValue(1'b0)
        ) sync_dst (
            .clk_i(src_clk),
            .rst_ni(src_rst_n),
            .serial_i(dst_ptr_gray[i]),
            .serial_o(dst_ptr_sync[i])
        );
    end
endgenerate
    gray_to_binary #(
        .N(ADDR_WIDTH+1)
    ) gray_to_bin_src_sync (
        .A(src_ptr_sync),
        .Z(src_ptr_sync_bin)
    );
    gray_to_binary #(
        .N(ADDR_WIDTH+1)
    ) gray_to_bin_dst_sync (
        .A(dst_ptr_sync),
        .Z(dst_ptr_sync_bin)
    );

    always_ff @(posedge src_clk)begin
        if(~src_rst_n)begin
            src_ptr <= 0;
        end
        else if(src_valid)begin
            src_ptr <= src_ptr_gray;
        end
    end

    always_ff @(posedge dst_clk)begin
        if(~dst_rst_n)begin
            dst_ptr <= 0;
        end
        else if(dst_valid & dst_ready)begin
            dst_ptr <= dst_ptr_gray;
        end
    end

    always_ff @(posedge src_clk)begin
        if(src_valid)begin
            mem[src_ptr_bin.idx] <= src_wdata;
        end
    end

    assign dst_valid = dst_ptr_bin != src_ptr_sync_bin;
    assign dst_rdata = mem[dst_ptr_bin.idx];

endmodule